iT邦幫忙

2024 iThome 鐵人賽

DAY 11
0

在過去的幾天裡,我們成功地實作家用品清單的新增和刪除功能。今天,我們要進一步讓這些資料不再只是暫時存在記憶體中,而是能夠永久儲存。為了實現這個目標,我們將學習如何在 SwiftUI 中使用 Core Data,將資料儲存在本地的手機中。

Core Data 是什麼?

Core Data 是蘋果提供的一個強大框架,用於管理本地資料的存取。它不僅僅是資料庫,而是一個物件圖形管理工具,可以幫助我們有效地管理 App 中的資料。我們可以用它來儲存結構化的資料,並且透過一系列 API 來輕鬆操作這些資料。

將 Core Data 整合到 SwiftUI 中

要在 SwiftUI 中使用 Core Data,我們首先需要在專案中加入 Core Data 支援。還記得在 Day2 時,我們在 Xcode 中建立專案,選擇加入 Core Data 功能。如果當初沒有一起選擇這個選項,也可以手動將 Core Data 整合進來。

啟用 Core Data

首先,檢查專案是否已經包含 Core Data 支援。如果還沒有,可以手動增加一個 xcdatamodeld 檔,這個檔案將作為我們的資料模型。
https://ooorito.com/wp-content/uploads/2024/08/%E6%AA%A2%E6%9F%A5CoreData.webp

如果當初建立專案時有加入 Core Data ,會像我一樣有一個 專案名+.xcdatamodeld 的檔案。

建立資料模型

開啟 xcdatamodeld 檔,然後在其中建立一個名為 Item 的實體。這個實體會包含我們要儲存的屬性,例如 name(名稱)和 quantity(數量)。

不過我們的家用品庫存 App 可不只是這麼簡單!除了記錄家用品外,我們還會提供到期日提醒、記帳、以及紀錄物品使用狀況等功能。因此,我們需要創建以下欄位:

  • dateAdded(Date):記錄物品加入清單的日期。
  • expiryDate(Date):物品的到期日。
  • id(UUID):物品的唯一識別碼。
  • isUsedUp(Boolean):標示物品是否已經用掉。
  • usedQuantity(Int16):記錄已經使用掉的數量。
  • name(String):物品的名稱。
  • price(Double):物品的價格。
  • quantity(Int16):物品的數量。

可能有人會疑惑為什麼需要 isUsedUp 和 usedQuantity 這兩個欄位。這是因為當物品用完後,如果直接刪除資料,可能會影響到我們的記帳記錄。所以不能直接從資料庫中刪除物品。

https://ooorito.com/wp-content/uploads/2024/08/Item%E8%B3%87%E6%96%99%E7%B5%90%E6%A7%8B-1024x447.webp

新增完成後,畫面應該會顯示成這樣。

新增 DataManager

在這個步驟中,我們引入了一個新的類別 DataManager,用來集中管理所有與 Core Data 相關的操作。DataManager 負責處理 CRUD(Create、Read、Update、Delete)操作,並將資料存取邏輯與 UI 邏輯分離,這樣可以讓我們的代碼結構更加清晰、模組化。

DataManager 是一個類別,這代表整個 App 都可以共用這個物件,保持資料的一致性。它將 NSPersistentContainer 封裝在 DataManager 中,負責 Core Data 的初始化和資料的保存。通過這樣的設計,我們的 ViewModel 可以更專注於 UI 層的邏輯,而不需要直接處理 Core Data 的細節。這也讓原本預設的 PersistenceController 可以刪除了。

import Foundation
import CoreData

class DataManager: NSObject, ObservableObject {
    
    // 初始化 Core Data 的容器,名稱與 xcdatamodeld 檔案一致
    let container: NSPersistentContainer = NSPersistentContainer(name: "YourName")
    
    override init() {
        super.init()
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Failed to load Core Data stack: \(error)")
            }
        }
    }
    
    // MARK: - CRUD Operations
    
    // Read (Fetch)
    func fetchItems() -> [Item] {
        let request: NSFetchRequest<Item> = Item.fetchRequest()
        do {
            return try container.viewContext.fetch(request)
        } catch {
            print("Failed to fetch items: \(error)")
            return []
        }
    }
    
    // Create
    func addItem(name: String, quantity: Int) {
        let newItem = Item(context: container.viewContext)
        newItem.id = UUID()
        newItem.name = name
        newItem.quantity = Int16(quantity)
        newItem.isUsedUp = false
        newItem.dateAdded = Date()
        newItem.price = 0.0
        newItem.usedQuantity = 0
        saveContext()
    }
    
    // Update
    func updateItem(item: Item, name: String, quantity: Int) {
        item.name = name
        item.quantity = Int16(quantity)
        saveContext()
    }
    
    // Delete
    func deleteItems(at offsets: IndexSet, items: [Item]) {
        offsets.map { items[$0] }.forEach(container.viewContext.delete)
        saveContext()
    }
    
    // Save Context
    private func saveContext() {
        let context = container.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

目前我們在新增項目時,只有兩個輸入欄位 - 名稱和數量,因此在 addItem 函式中,暫時將未設置的欄位使用預設值,這樣可以避免執行時發生閃退。明天,我們將補齊這些欄位,並改寫這部分的程式碼,讓我們的 App 更加完善!

更新 ViewModel

因為我們的資料來自 Core Data,所以我們需要更新 ItemViewModel,才能與 Core Data 進行互動。我們將使用 DataManager 來管理資料的儲存和刪除。

import SwiftUI

class ItemViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var newItemName: String = ""
    @Published var newItemQuantity: String = ""

    private let dataManager: DataManager

    init(dataManager: DataManager = DataManager()) {
        self.dataManager = dataManager
        fetchItems()
    }

    func fetchItems() {
        items = dataManager.fetchItems()
    }

    func addItem(name: String, quantity: Int) {
        dataManager.addItem(name: name, quantity: quantity)
        fetchItems()
    }

    func deleteItems(at offsets: IndexSet) {
        dataManager.deleteItems(at: offsets, items: items)
        fetchItems()
    }
}

我們將剛剛編寫的 DataManager 引入到專案中,讓基本的 CRUD 操作都交由 DataManager 處理,而不需要在 ViewModel 中直接處理。

在程式碼中,init(dataManager: DataManager = DataManager()) 是一個依賴注入的小技巧,這樣做的目的是讓未來撰寫單元測試更方便。至於什麼是依賴注入,我們會在後面的章節中介紹,敬請期待!

在 ContentView 中使用

在 ContentView 中,我們將使用 ItemViewModel 來管理和顯示資料,讓所有資料的變動都能更新在 UI 上。

import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel: ItemViewModel = ItemViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                HStack {
                    TextField("輸入家用品名稱", text: $viewModel.newItemName)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()
                    
                    TextField("數量", text: $viewModel.newItemQuantity)
                        .keyboardType(.numberPad)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()
                    
                    Button(action: {
                        viewModel.addItem(name: viewModel.newItemName, quantity: Int(viewModel.newItemQuantity) ?? 0)
                    }) {
                        Text("新增")
                            .padding(.horizontal, 12)
                            .padding(.vertical, 8)
                            .background(Color.blue)
                            .foregroundColor(.white)
                            .cornerRadius(8)
                    }
                }
                
                List {
                    ForEach(viewModel.items) { item in
                        HStack {
                            Text(item.name)
                            Spacer()
                            Text("數量: \(item.quantity)")
                        }
                    }
                    .onDelete(perform: viewModel.deleteItems)
                }
                .navigationTitle("家用品清單")
            }
        }
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let dataManager = DataManager()
        let viewContext = dataManager.container.viewContext

        let viewModel = ItemViewModel(dataManager: dataManager)

        return ContentView()
            .environment(\.managedObjectContext, viewContext)
    }
}

更新 HandyInventory_ironApp

在更新 App 的入口,我們需要將 DataManager 注入到 SwiftUI 的環境中,這樣在整個 App 中都可以方便地使用 Core Data 的 viewContext。具體程式碼如下:

import SwiftUI

@main
struct HandyInventory_ironApp: App {
    let dataManager = DataManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, dataManager.container.viewContext)
        }
    }
}

在這段程式碼中,我們做了以下幾件事:

  • 建立了 DataManager 的物件 dataManager,這個物件會負責管理 Core Data 的所有操作。
  • 透過 .environment(\.managedObjectContext, dataManager.container.viewContext),將 DataManager 的 viewContext 注入到 SwiftUI 的環境中。這樣,我們的 ContentView 以及其子畫面都能夠透過 @Environment(\.managedObjectContext) 獲取這個上下文,並進行資料操作。

這樣設置之後,我們的 App 就可以在各個畫面中輕鬆使用 Core Data 進行資料的存取和管理。

通過以上步驟,我們將資料儲存到了 Core Data 中,這意味著即使 App 關閉,這些家用品項目也會持續存在。當 App 重新打開時,這些資料將會自動從 Core Data 中載入並顯示在清單中。讓我們來看看成果吧!

https://ooorito.com/wp-content/uploads/2024/08/%E6%88%90%E6%9E%9C.gif

總結

這樣一來,我們的 App 不再是暫時性的清單,而是一個能夠永久儲存資料的實用工具。透過 DataManager 的整合,我們可以簡單地管理 Core Data 的操作,並將資料存儲邏輯與 UI 邏輯分離。今天先寫到這裡,我們明天見!

參考資料:SwiftUI & Core Data


上一篇
Day 10: 在 SwiftUI 中實現新增與刪除家用品項目
下一篇
Day 12: SwiftUI 新增項目頁面與懸浮按鈕設計
系列文
用 SwiftUI 掌控家庭日用品庫存30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言